Welcome to my_other!

import sys

import os

import json

import time

import traceback

import pandas as pd

import numpy as np

from datetime import datetime, timedelta

from sqlalchemy import create_engine

from PySide6.QtWidgets import (

QApplication, QMainWindow, QDialog, QVBoxLayout, QHBoxLayout,

QPushButton, QLineEdit, QTextEdit, QLabel, QFileDialog, QMessageBox,

QMenuBar, QMenu, QWidget, QFormLayout, QComboBox, QSpinBox

)

from PySide6.QtCore import Qt, QThread, Signal

# ==============================================================================

# 【区域一:全局配置与辅助函数(新增代码,三段式注解)】

# ==============================================================================

CONFIG_FILE = "task_config.json"

def load_all_config():

"""

从 JSON 文件加载所有任务的记忆参数。

"""

# ===== 第 1 部分:尝试打开配置文件并读取 JSON =====

try:

with open(CONFIG_FILE, 'r', encoding='utf-8') as f: # ① 以只读模式打开配置文件

return json.load(f) # ② 将文件内容解析为 Python 字典

# ===== 作用总结:如果配置文件存在且内容合法,返回记忆参数字典 =====

# ===== 第 2 部分:处理文件不存在或格式错误的情况 =====

except (FileNotFoundError, json.JSONDecodeError):

return {} # ① 返回空字典,表示没有任何记忆参数

# ===== 作用总结:保证程序启动时不会因为配置文件问题而崩溃 =====

def save_all_config(config_dict):

"""

将所有任务的记忆参数保存到 JSON 文件。

"""

# ===== 第 1 部分:将字典写入 JSON 文件 =====

with open(CONFIG_FILE, 'w', encoding='utf-8') as f: # ① 以写入模式打开文件

json.dump(config_dict, f, indent=4, ensure_ascii=False) # ② 将字典序列化并格式化写入

# ===== 作用总结:将当前所有任务的参数值永久保存到硬盘,供下次启动时读取 =====

# ==============================================================================

# 【区域二:任务定义列表(新增代码,三段式注解)】

# ==============================================================================

# 每个任务由以下字段描述:

# - id : 唯一标识符,用于记忆参数

# - name : 显示在菜单上的名称

# - category : 菜单分类(如 "数据清洗"、"数据分析"),用于分组

# - description : 任务简短描述(鼠标悬停提示)

# - params : 参数列表,每个参数是一个字典,包含:

# * name : 参数变量名(传给处理函数的键名)

# * label : 界面显示标签

# * type : 参数类型,支持 "file"(文件选择)、"folder"(文件夹选择)、"text"(普通文本)、"int"(整数)、"combo"(下拉框)

# * default : 默认值(若配置文件中无记忆值)

# * required : 是否必填(True/False)

# * options : 仅当 type 为 "combo" 时有效,列表形式,如 ["选项1","选项2"]

# - function : 实际执行数据处理的函数名称(字符串,稍后映射到实际函数)

TASK_LIST = [

{

"id": "transaction_import",

"name": "交易记录整理写入",

"category": "数据清洗",

"description": "从导出的Excel文件整理交易记录并写入数据库",

"params": [

{"name": "path_ot", "label": "交易记录文件", "type": "file", "default": "", "required": True},

{"name": "path_itp", "label": "采购商清单文件", "type": "file", "default": "", "required": True},

{"name": "path_its", "label": "客户清单文件", "type": "file", "default": "", "required": True},

],

"function": "task_transaction_import"

},

{

"id": "weighted_avg_price",

"name": "加权平均价计算",

"category": "数据分析",

"description": "从数据库读取销售订单,计算近三年加权平均价并导出Excel",

"params": [

{"name": "db_host", "label": "数据库主机", "type": "text", "default": "localhost", "required": True},

{"name": "db_port", "label": "端口", "type": "int", "default": 3306, "required": True},

{"name": "db_user", "label": "用户名", "type": "text", "default": "root", "required": True},

{"name": "db_password", "label": "密码", "type": "text", "default": "502", "required": True},

{"name": "db_name", "label": "数据库名", "type": "text", "default": "shn", "required": True},

{"name": "output_dir", "label": "输出文件夹", "type": "folder", "default": "", "required": True},

],

"function": "task_weighted_avg_price"

},

]

# ==============================================================================

# 【区域三:用户原始数据处理函数(用户提供,不做修改,不添加注解)】

# ==============================================================================

def task_transaction_import(params, log_signal, stop_flag):

"""

===== 用户提供的第一段代码:交易记录整理写入 =====

"""

import pandas as pd

import numpy as np

from sqlalchemy import create_engine

import time

try:

log_signal.emit("开始读取交易记录文件...")

df_ot = pd.read_excel(params["path_ot"])

if stop_flag['stop']: return

log_signal.emit("开始读取采购商清单...")

df_itp = pd.read_excel(params["path_itp"])

if stop_flag['stop']: return

log_signal.emit("开始读取客户清单...")

df_its = pd.read_excel(params["path_its"])

if stop_flag['stop']: return

log_signal.emit("正在合并数据...")

df_itp1 = df_itp.loc[:, ["供应商帐户", "供应商分类", "名称"]]

df_its1 = df_its.loc[:, ["客户帐户", "名称", "搜索名称", "客户录入", "隶属集团客户"]]

df1 = pd.merge(

left=df_ot, right=df_itp1, how='left',

left_on="帐号", right_on="供应商帐户"

).rename(columns={"名称": "供应商名称"}).drop("供应商帐户", axis=1)

df2 = pd.merge(

left=df1, right=df_its1, how='left',

left_on="帐号", right_on="客户帐户"

).rename(columns={"名称": "客户名称"}).drop("客户帐户", axis=1)

del df_itp, df_its

if stop_flag['stop']: return

def return_row(group):

for _, row in group.iterrows():

if row['引用'] in ('生产', '采购订单', '销售订单'):

return row

return None

log_signal.emit("正在处理订单引用...")

pro = df_ot.groupby('编号').apply(return_row)

del df_ot

pro1 = pro.iloc[:, 2]

df_pro = pd.merge(df2, pro1, how='left', left_on="编号", right_on="编号")

df_pro1 = df_pro.rename(columns={"物料编号_x": "物料编号", "物料编号_y": "产品编号"}).reset_index()

df_pro1["年月"] = pd.to_datetime(df_pro1.财务日期).dt.strftime("%Y%m")

df_pro1["年"] = pd.to_datetime(df_pro1.财务日期).dt.strftime("%Y")

df_pro1 = df_pro1.drop("index", axis=1)

df_pro1["调整"] = df_pro1["调整"].fillna(0)

df_pro1["成本额"] = df_pro1["成本额"].fillna(0)

df_pro1["总成本金额"] = df_pro1["成本额"] + df_pro1["调整"]

column_order = [

"引用", "编号", "物料编号", "交易记录ID", "仓库", "批处理号", "库位", "类型", "数量",

"实际日期", "财务日期", "实际成本额", "成本额", "调整", "站点", "财务凭证", "实际凭证",

"帐号", "总成本金额", "年", "年月", "供应商分类", "供应商名称", "客户名称",

"搜索名称", "客户录入", "隶属集团客户", "产品编号"

]

df_result = df_pro1[column_order].rename(columns={

"引用": "ct_mobileType", "编号": "ct_PO", "物料编号": "ct_ItemRelation",

"交易记录ID": "ct_id", "仓库": "ct_warehouse", "批处理号": "ct_batchNumber",

"库位": "ct_warehouseLocation", "类型": "ct_productionType", "数量": "ct_quantity",

"实际日期": "ct_actualDate", "财务日期": "ct_financialDate",

"实际成本额": "ct_actualCostAmount", "成本额": "ct_costAmount",

"调整": "ct_adjustingCosts", "站点": "ct_dataAreaId",

"财务凭证": "ct_financialVouchers", "实际凭证": "ct_actualVoucher",

"帐号": "ct_accounts", "总成本金额": "ct_totalCostAmount", "年": "ct_year",

"年月": "ct_yearAndMonth", "供应商分类": "ct_supplierClassification",

"供应商名称": "ct_supplierName", "客户名称": "ct_customerName",

"搜索名称": "ct_customerAbbreviation", "客户录入": "ct_customerInput",

"隶属集团客户": "ct_affiliatedGroupCustomers", "产品编号": "ct_productNumber"

})

if stop_flag['stop']: return

log_signal.emit("正在写入数据库...")

engine = create_engine('mysql+mysqlconnector://root:502@localhost:3306/shn')

df_result.to_sql('ct', con=engine, if_exists='append', index=False)

log_signal.emit("数据库写入完成。")

except Exception as e:

log_signal.emit(f"处理出错: {str(e)}\n{traceback.format_exc()}")

def task_weighted_avg_price(params, log_signal, stop_flag):

"""

===== 用户提供的第二段代码:加权平均价计算 =====

"""

import pandas as pd

import numpy as np

from sqlalchemy import create_engine

from datetime import datetime, timedelta

import os

try:

conn_str = f"mysql+mysqlconnector://{params['db_user']}:{params['db_password']}@{params['db_host']}:{params['db_port']}/{params['db_name']}"

engine = create_engine(conn_str)

log_signal.emit("已连接到数据库。")

log_signal.emit("正在读取销售订单数据...")

dfs0 = pd.read_sql('SELECT * FROM ct WHERE ct_mobileType = "销售订单"', con=engine)

if stop_flag['stop']: return

def get_last_day_of_last_month(date):

first_day_of_month = date.replace(day=1)

return first_day_of_month - timedelta(days=1)

today = datetime.now()

last_day = get_last_day_of_last_month(today)

y3 = pd.to_datetime(last_day)

three_years_ago = y3 - pd.DateOffset(years=3)

log_signal.emit(f"计算基准日期:上月末 {last_day.strftime('%Y-%m-%d')},三年前 {three_years_ago.strftime('%Y-%m-%d')}")

dfs0['ct_financialDate'] = pd.to_datetime(dfs0['ct_financialDate'])

dfs0['is_recent'] = dfs0['ct_financialDate'] > three_years_ago

recent_df = dfs0[dfs0['is_recent']].copy()

older_df = dfs0[~dfs0['is_recent']].copy()

recent_df['ct_totalCostAmount'] = recent_df['ct_totalCostAmount'].fillna(0)

older_df['ct_totalCostAmount'] = older_df['ct_totalCostAmount'].fillna(0)

recent_df = recent_df[recent_df.ct_totalCostAmount != 0]

older_df = older_df[older_df.ct_totalCostAmount != 0]

if stop_flag['stop']: return

recent_df["avge"] = recent_df.ct_totalCostAmount / recent_df.ct_quantity

older_df["avge"] = older_df.ct_totalCostAmount / older_df.ct_quantity

dfr1 = recent_df.groupby("ct_ItemRelation").agg(

总金额=("ct_totalCostAmount", "sum"),

总数量=("ct_quantity", "sum"),

最大平均值=("avge", "max"),

最小平均值=("avge", "min")

).reset_index()

dfr1["平均值"] = dfr1["总金额"] / dfr1["总数量"]

dfo1 = older_df.groupby("ct_ItemRelation").agg(

总金额=("ct_totalCostAmount", "sum"),

总数量=("ct_quantity", "sum"),

最大平均值=("avge", "max"),

最小平均值=("avge", "min")

).reset_index()

dfo1["平均值"] = dfo1["总金额"] / dfo1["总数量"]

dfo1 = dfo1.drop_duplicates('ct_ItemRelation')

dfo1["价格状态"] = "近3年历史价格"

all_items = dfs0[["ct_ItemRelation"]].drop_duplicates()

recent_items = dfr1["ct_ItemRelation"].unique()

old_only_items = all_items[~all_items["ct_ItemRelation"].isin(recent_items)].copy()

old_only_items["价格状态"] = "3年前历史价格"

dfs_old_only = pd.merge(old_only_items, dfo1, on="ct_ItemRelation", how="left")

dfo1["价格类型"] = "销售价"

dfs_old_only["价格类型"] = "销售价"

final_result = pd.concat([dfo1, dfs_old_only], ignore_index=True)

if stop_flag['stop']: return

output_dir = params["output_dir"]

os.makedirs(output_dir, exist_ok=True)

timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

recent_df.to_excel(os.path.join(output_dir, f"近三年销售明细_{timestamp}.xlsx"), index=False)

older_df.to_excel(os.path.join(output_dir, f"三年前销售明细_{timestamp}.xlsx"), index=False)

final_result.to_excel(os.path.join(output_dir, f"加权平均价结果_{timestamp}.xlsx"), index=False)

log_signal.emit(f"结果已保存至文件夹:{output_dir}")

except Exception as e:

log_signal.emit(f"处理出错: {str(e)}\n{traceback.format_exc()}")

# 将函数名字符串映射到实际函数对象(新增代码,三段式注解)

FUNCTION_MAP = {

# ===== 第 1 部分:键是字符串 =====

"task_transaction_import": task_transaction_import, # ① 映射到交易记录处理函数

"task_weighted_avg_price": task_weighted_avg_price, # ② 映射到加权平均价计算函数

# ===== 作用总结:通过字典将配置中的函数名字符串转换为可调用的函数对象 =====

}

# ==============================================================================

# 【区域四:通用工作线程类(新增代码,三段式注解)】

# ==============================================================================

class WorkerThread(QThread):

"""

后台工作线程,负责执行耗时的数据处理任务,不阻塞界面。

"""

# ===== 第 1 部分:定义信号 =====

log_signal = Signal(str) # ① 创建一个能发送字符串的信号,用于向主界面发送日志

finished_signal = Signal() # ② 创建一个无参数的信号,用于通知任务结束

# ===== 作用总结:通过信号实现线程安全的界面更新 =====

def __init__(self, task_func, params):

"""

参数:

task_func : 要执行的处理函数

params : 参数字典

"""

# ===== 第 1 部分:调用父类构造函数 =====

super().__init__() # ① 必须调用 QThread 的初始化,使对象具备线程能力

# ===== 作用总结:确保线程对象正确创建 =====

# ===== 第 2 部分:保存传入的参数 =====

self.task_func = task_func # ① 将处理函数保存为实例变量,供 run() 调用

self.params = params # ② 将参数字典保存为实例变量

# ===== 作用总结:让线程在运行时能够访问到需要执行的函数和参数 =====

# ===== 第 3 部分:创建停止标志字典 =====

self.stop_flag = {'stop': False} # ① 使用字典包装布尔值,使函数内部修改能被外部感知

# ===== 作用总结:提供一个可变容器,用于在任务执行过程中传递停止指令 =====

def run(self):

"""

线程启动后自动执行的函数。此方法在后台线程中运行。

"""

# ===== 第 1 部分:记录开始时间 =====

start_time = time.time() # ① 获取当前时间戳,用于计算总耗时

# ===== 作用总结:为稍后输出运行时长做准备 =====

# ===== 第 2 部分:发送开始日志 =====

self.log_signal.emit("任务开始执行...") # ① 通过信号将字符串发送到主界面

# ===== 作用总结:让用户知道任务已启动 =====

# ===== 第 3 部分:执行用户提供的处理函数 =====

try:

self.task_func(self.params, self.log_signal, self.stop_flag) # ① 调用处理函数,传入参数、日志信号和停止标志

except Exception as e:

self.log_signal.emit(f"未捕获的异常: {str(e)}") # ② 如果处理函数抛出异常,捕获并显示

# ===== 作用总结:实际执行用户的数据处理逻辑 =====

# ===== 第 4 部分:任务结束后的处理 =====

finally:

if not self.stop_flag['stop']: # ① 检查是否因用户停止而结束

duration = time.time() - start_time # ② 计算运行时长

self.log_signal.emit(f"运行时长: {duration:.2f}秒") # ③ 发送耗时信息

self.finished_signal.emit() # ④ 发送任务完成信号

# ===== 作用总结:正常结束时显示耗时,并通知主界面任务已结束 =====

def stop(self):

"""

供外部调用的停止方法。

"""

# ===== 第 1 部分:修改停止标志 =====

self.stop_flag['stop'] = True # ① 将字典中的布尔值设为 True

# ===== 作用总结:处理函数内部会定期检查该标志,发现为 True 时提前退出 =====

# ===== 第 2 部分:发送日志提示 =====

self.log_signal.emit("正在中止,请稍候...") # ① 通过信号告知用户已收到停止指令

# ===== 作用总结:提供界面反馈,表明停止操作已被接受 =====

# ==============================================================================

# 【区域五:动态参数输入对话框类(新增代码,三段式注解)】

# ==============================================================================

class TaskDialog(QDialog):

"""

根据任务配置动态生成参数输入界面的对话框。

"""

def __init__(self, task_config, parent=None):

# ===== 第 1 部分:调用父类构造函数并设置窗口属性 =====

super().__init__(parent) # ① 初始化 QDialog 基类

self.task_config = task_config # ② 保存传入的任务配置字典

self.setWindowTitle(task_config["name"]) # ③ 设置对话框标题为任务名称

self.setMinimumWidth(600) # ④ 设置最小宽度,防止布局变形

# ===== 作用总结:创建一个标题为任务名称的对话框窗口 =====

# ===== 第 2 部分:加载该任务的记忆参数 =====

self.all_config = load_all_config() # ① 从文件读取所有任务的记忆字典

task_id = task_config["id"] # ② 获取当前任务的唯一标识符

self.saved_params = self.all_config.get(task_id, {}) # ③ 从总配置中取出该任务的记忆参数,若无则为空字典

# ===== 作用总结:获取该任务上次保存的参数值,用于预填充输入框 =====

# ===== 第 3 部分:初始化成员变量 =====

self.param_widgets = {} # ① 字典,用于存储参数名与对应输入控件的映射,方便后续取值

self.worker = None # ② 初始化为 None,表示当前没有后台线程在运行

# ===== 作用总结:为界面控件管理和线程控制做准备 =====

# ===== 第 4 部分:调用界面构建方法 =====

self.setup_ui() # ① 将界面搭建逻辑分离到单独的方法中

# ===== 作用总结:让 __init__ 方法保持简洁,界面代码集中管理 =====

def setup_ui(self):

"""

构建对话框界面:参数表单 + 日志区域 + 按钮栏。

"""

# ===== 第 1 部分:创建主垂直布局 =====

main_layout = QVBoxLayout(self) # ① 将垂直布局设置为对话框的主布局

# ===== 作用总结:后续所有控件都将按从上到下的顺序排列 =====

# ----- 参数表单区域(使用 QFormLayout 整齐排列标签和输入控件)-----

# ===== 第 2 部分:创建表单布局容器 =====

form_widget = QWidget() # ① 创建一个空白 QWidget 作为表单容器

form_layout = QFormLayout(form_widget) # ② 为该容器设置表单布局(两列:标签列+输入控件列)

form_layout.setLabelAlignment(Qt.AlignRight) # ③ 设置标签文字右对齐,更美观

# ===== 作用总结:准备好一个两列表单,用于放置参数标签和对应的输入控件 =====

# ===== 第 3 部分:遍历任务配置中的参数列表,动态创建输入控件 =====

for param_def in self.task_config["params"]:

param_name = param_def["name"] # ① 参数变量名

param_label = param_def["label"] # ② 界面显示标签

param_type = param_def["type"] # ③ 参数类型(file/folder/text/int/combo)

default_val = self.saved_params.get(param_name, param_def.get("default", "")) # ④ 获取默认值:优先使用记忆值,其次配置中的默认值

# 根据参数类型创建不同的输入控件

if param_type == "file":

# --- 文件选择:水平布局包含 QLineEdit 和“浏览”按钮 ---

# ===== 第 4-1 部分:创建文件选择控件 =====

container = QWidget() # ① 创建一个容器部件

h_layout = QHBoxLayout(container) # ② 为容器设置水平布局

h_layout.setContentsMargins(0, 0, 0, 0) # ③ 去除布局边距,使控件紧凑

edit = QLineEdit(str(default_val)) # ④ 创建文本输入框,并填入默认值

btn = QPushButton("浏览...") # ⑤ 创建“浏览”按钮

btn.clicked.connect(lambda checked, e=edit, p=param_def: self.browse_file(e, p)) # ⑥ 连接点击信号,调用 browse_file 方法

h_layout.addWidget(edit) # ⑦ 将输入框加入水平布局

h_layout.addWidget(btn) # ⑧ 将按钮加入水平布局

form_layout.addRow(param_label + ":", container) # ⑨ 将整行添加到表单布局

self.param_widgets[param_name] = edit # ⑩ 将输入框保存到控件字典中

# ===== 作用总结:创建一个带“浏览”按钮的文件路径输入行 =====

elif param_type == "folder":

# --- 文件夹选择:水平布局包含 QLineEdit 和“浏览”按钮 ---

container = QWidget()

h_layout = QHBoxLayout(container)

h_layout.setContentsMargins(0, 0, 0, 0)

edit = QLineEdit(str(default_val))

btn = QPushButton("浏览...")

btn.clicked.connect(lambda checked, e=edit: self.browse_folder(e))

h_layout.addWidget(edit)

h_layout.addWidget(btn)

form_layout.addRow(param_label + ":", container)

self.param_widgets[param_name] = edit

# ===== 作用总结:创建一个带“浏览”按钮的文件夹路径输入行 =====

elif param_type == "int":

# --- 整数输入:使用 QSpinBox ---

spin = QSpinBox() # ① 创建整数调节框

spin.setRange(0, 999999) # ② 设置取值范围

spin.setValue(int(default_val) if default_val else 0) # ③ 设置默认值

form_layout.addRow(param_label + ":", spin) # ④ 添加到表单

self.param_widgets[param_name] = spin # ⑤ 保存控件引用

# ===== 作用总结:创建一个只能输入整数的调节框 =====

elif param_type == "combo":

# --- 下拉选择:QComboBox ---

combo = QComboBox() # ① 创建下拉选择框

combo.addItems(param_def.get("options", [])) # ② 添加选项列表

if default_val in param_def.get("options", []):

combo.setCurrentText(default_val) # ③ 如果默认值在选项中,设为当前选中项

form_layout.addRow(param_label + ":", combo)

self.param_widgets[param_name] = combo

# ===== 作用总结:创建一个下拉选择框,用户只能从预设选项中选择 =====

else: # 默认为文本输入

# --- 普通文本输入:QLineEdit ---

edit = QLineEdit(str(default_val)) # ① 创建文本输入框

form_layout.addRow(param_label + ":", edit)

self.param_widgets[param_name] = edit

# ===== 作用总结:创建一个普通的单行文本输入框 =====

main_layout.addWidget(form_widget) # 将表单区域加入主布局

# ----- 日志区域 -----

# ===== 第 5 部分:创建日志显示框 =====

main_layout.addWidget(QLabel("运行日志:")) # ① 添加一个标签“运行日志:”

self.log_text = QTextEdit() # ② 创建多行文本编辑框

self.log_text.setReadOnly(True) # ③ 设置为只读,用户不可编辑

main_layout.addWidget(self.log_text) # ④ 加入主布局

# ===== 作用总结:添加一个只读的多行文本框,用于显示处理过程中的日志信息 =====

# ----- 底部按钮栏 -----

# ===== 第 6 部分:创建底部按钮栏 =====

btn_layout = QHBoxLayout() # ① 创建水平布局用于放置按钮

self.run_btn = QPushButton("运行") # ② 创建“运行”按钮

self.run_btn.clicked.connect(self.start_processing) # ③ 连接点击信号到槽函数

btn_layout.addWidget(self.run_btn)

self.stop_btn = QPushButton("停止") # ④ 创建“停止”按钮

self.stop_btn.setEnabled(False) # ⑤ 初始状态为禁用(灰色)

self.stop_btn.clicked.connect(self.stop_processing) # ⑥ 连接点击信号

btn_layout.addWidget(self.stop_btn)

self.close_btn = QPushButton("关闭") # ⑦ 创建“关闭”按钮

self.close_btn.clicked.connect(self.close) # ⑧ 连接点击信号到对话框的 close 方法

btn_layout.addWidget(self.close_btn)

main_layout.addLayout(btn_layout) # ⑨ 将按钮栏加入主布局

# ===== 作用总结:添加“运行”、“停止”、“关闭”三个操作按钮 =====

# --------------------------------------------------------------------------

# 辅助方法:文件/文件夹浏览(新增代码,三段式注解)

# --------------------------------------------------------------------------

def browse_file(self, line_edit, param_def):

"""

弹出文件选择对话框,并将选中的文件路径填入输入框。

"""

# ===== 第 1 部分:弹出文件选择对话框 =====

file_filter = "Excel 文件 (*.xlsx *.xls);;所有文件 (*.*)" # ① 定义文件类型过滤器

file_path, _ = QFileDialog.getOpenFileName(

self, # ② 父窗口

f"选择 {param_def['label']}", # ③ 对话框标题

line_edit.text() or os.getcwd(), # ④ 默认打开路径:若输入框有内容则用其目录,否则用当前工作目录

file_filter # ⑤ 文件过滤器

)

# ===== 作用总结:让用户通过图形界面选择一个文件,返回其完整路径字符串 =====

# ===== 第 2 部分:将选中的路径填入输入框 =====

if file_path: # ① 如果用户没有取消(即选择了文件)

line_edit.setText(file_path) # ② 将输入框内容更新为选中的文件路径

# ===== 作用总结:把用户选择的路径显示在界面上 =====

def browse_folder(self, line_edit):

"""

弹出文件夹选择对话框,并将选中的文件夹路径填入输入框。

"""

# ===== 第 1 部分:弹出文件夹选择对话框 =====

folder = QFileDialog.getExistingDirectory(

self, # ① 父窗口

"选择文件夹", # ② 对话框标题

line_edit.text() or os.getcwd() # ③ 默认打开路径

)

# ===== 作用总结:让用户选择一个文件夹,返回其完整路径 =====

# ===== 第 2 部分:将选中的路径填入输入框 =====

if folder: # ① 如果用户没有取消

line_edit.setText(folder) # ② 更新输入框内容

# ===== 作用总结:把用户选择的文件夹路径显示在界面上 =====

# --------------------------------------------------------------------------

# 收集参数值并校验(新增代码,三段式注解)

# --------------------------------------------------------------------------

def collect_params(self):

"""

从界面控件读取用户输入,返回参数字典。若必填项为空,返回 None。

"""

# ===== 第 1 部分:遍历所有参数定义,从控件中读取值 =====

params = {}

for param_def in self.task_config["params"]:

name = param_def["name"] # ① 参数名

widget = self.param_widgets[name] # ② 对应的输入控件

if isinstance(widget, QLineEdit):

value = widget.text().strip() # ③ 文本框取值并去除首尾空格

elif isinstance(widget, QSpinBox):

value = widget.value() # ④ 调节框取值(整数)

elif isinstance(widget, QComboBox):

value = widget.currentText() # ⑤ 下拉框取值(字符串)

else:

value = ""

# ===== 第 2 部分:必填校验 =====

if param_def.get("required", False) and not value: # ① 如果该参数标记为必填且值为空

QMessageBox.warning(self, "警告", f"参数 '{param_def['label']}' 不能为空!")

return None # ② 校验失败,返回 None

# ===== 作用总结:确保所有必填参数都已填写,否则提示用户并终止 =====

params[name] = value # ③ 将有效的参数值存入字典

# ===== 作用总结:返回一个包含所有参数名和对应值的字典 =====

return params

# --------------------------------------------------------------------------

# 启动处理(新增代码,三段式注解)

# --------------------------------------------------------------------------

def start_processing(self):

"""

用户点击“运行”按钮后执行。

"""

# ===== 第 1 部分:收集并校验参数 =====

params = self.collect_params()

if params is None:

return # 校验失败,直接返回

# ===== 作用总结:获取用户输入的所有参数,若校验不通过则停止执行 =====

# ===== 第 2 部分:保存当前参数到配置文件(记忆功能) =====

task_id = self.task_config["id"] # ① 获取当前任务 ID

self.all_config[task_id] = params # ② 将本次参数存入总配置字典

save_all_config(self.all_config) # ③ 将总配置字典写入 JSON 文件

# ===== 作用总结:实现参数记忆,下次打开该任务时自动填充本次使用的值 =====

# ===== 第 3 部分:切换按钮状态,清空日志 =====

self.run_btn.setEnabled(False) # ① “运行”按钮变灰,防止重复点击

self.stop_btn.setEnabled(True) # ② “停止”按钮变为可用

self.log_text.clear() # ③ 清空上一次的日志内容

# ===== 作用总结:界面进入“运行中”状态,准备显示新的日志 =====

# ===== 第 4 部分:获取实际处理函数 =====

func_name = self.task_config["function"] # ① 从任务配置中取出函数名字符串

task_func = FUNCTION_MAP.get(func_name) # ② 通过映射字典获取真正的函数对象

if task_func is None: # ③ 如果找不到函数

QMessageBox.critical(self, "错误", f"未找到处理函数 '{func_name}'")

self.run_btn.setEnabled(True) # ④ 恢复按钮状态

self.stop_btn.setEnabled(False)

return

# ===== 作用总结:根据配置找到要执行的处理函数,若配置错误则提示并恢复界面 =====

# ===== 第 5 部分:创建并启动工作线程 =====

self.worker = WorkerThread(task_func, params) # ① 实例化工作线程,传入处理函数和参数

self.worker.log_signal.connect(self.append_log) # ② 连接日志信号到槽函数,实现日志显示

self.worker.finished_signal.connect(self.on_worker_finished) # ③ 连接完成信号到槽函数

self.worker.start() # ④ 启动线程

# ===== 作用总结:在后台启动数据处理任务,界面保持响应 =====

# --------------------------------------------------------------------------

# 停止处理(新增代码,三段式注解)

# --------------------------------------------------------------------------

def stop_processing(self):

"""

用户点击“停止”按钮后执行。

"""

# ===== 第 1 部分:检查是否有正在运行的线程 =====

if self.worker and self.worker.isRunning(): # ① worker 非空且线程正在运行

self.worker.stop() # ② 调用工作线程的 stop() 方法,设置停止标志

self.stop_btn.setEnabled(False) # ③ 立即禁用停止按钮,防止重复点击

# ===== 作用总结:向后台线程发送停止指令,并让停止按钮变灰 =====

# --------------------------------------------------------------------------

# 线程完成后的清理工作(新增代码,三段式注解)

# --------------------------------------------------------------------------

def on_worker_finished(self):

"""

当工作线程执行完毕后自动调用。

"""

# ===== 第 1 部分:恢复按钮状态 =====

self.run_btn.setEnabled(True) # ① “运行”按钮重新可用

self.stop_btn.setEnabled(False) # ② “停止”按钮恢复禁用状态

# ===== 作用总结:界面恢复为就绪状态,等待用户下一次操作 =====

# ===== 第 2 部分:追加结束提示并弹窗 =====

self.append_log("任务线程已结束。") # ① 在日志最后添加一行明确提示

QMessageBox.information(self, "提示", "任务执行完毕,请查看日志。") # ② 弹出信息框提醒用户

# ===== 作用总结:明确告知用户任务已结束 =====

# --------------------------------------------------------------------------

# 向日志框追加文本(新增代码,三段式注解)

# --------------------------------------------------------------------------

def append_log(self, text):

"""

接收信号传来的字符串,显示在日志框中。

"""

# ===== 第 1 部分:调用 QTextEdit 的 append 方法 =====

self.log_text.append(text) # ① 在日志框末尾追加一行文本,并自动换行

# ===== 作用总结:实时显示后台处理进度和结果 =====

# --------------------------------------------------------------------------

# 重写关闭事件(新增代码,三段式注解)

# --------------------------------------------------------------------------

def closeEvent(self, event):

"""

当用户点击右上角“X”或调用 close() 时触发。

"""

# ===== 第 1 部分:检查是否有任务正在运行 =====

if self.worker and self.worker.isRunning(): # ① 如果后台线程还在跑

reply = QMessageBox.question(

self, "确认", "任务正在运行,确定要关闭窗口吗?",

QMessageBox.Yes | QMessageBox.No

) # ② 弹出询问对话框,让用户二选一

# ===== 作用总结:防止用户误关窗口导致后台线程残留 =====

# ===== 第 2 部分:根据用户选择决定是否关闭 =====

if reply == QMessageBox.Yes: # ① 用户选择“是”

self.worker.stop() # ② 发送停止指令

self.worker.wait(2000) # ③ 等待最多2秒,让线程有机会退出

event.accept() # ④ 接受关闭事件,窗口正常关闭

else:

event.ignore() # ⑤ 用户选择“否”,忽略事件,窗口不关闭

else:

event.accept() # ⑥ 没有任务运行,直接关闭

# ===== 作用总结:确保关闭窗口前妥善处理后台线程,避免程序崩溃或资源泄露 =====

# ==============================================================================

# 【区域六:主窗口类(新增代码,三段式注解)】

# ==============================================================================

class MainWindow(QMainWindow):

"""

应用程序主窗口,包含菜单栏,用于启动各个任务。

"""

def __init__(self):

# ===== 第 1 部分:调用父类构造函数并设置窗口属性 =====

super().__init__() # ① 初始化 QMainWindow 基类

self.setWindowTitle("数据处理工具集") # ② 设置主窗口标题

self.setMinimumSize(400, 300) # ③ 设置最小尺寸

# ===== 作用总结:创建一个标题为“数据处理工具集”的主窗口 =====

# ===== 第 2 部分:创建菜单栏并动态生成分类菜单 =====

menubar = self.menuBar() # ① 获取窗口的菜单栏对象

category_menus = {} # ② 字典,用于存储已创建的分类菜单对象

for task in TASK_LIST: # ③ 遍历任务配置列表

category = task.get("category", "默认分类") # ④ 获取任务的分类,若无则用“默认分类”

if category not in category_menus: # ⑤ 如果该分类菜单尚未创建

category_menus[category] = menubar.addMenu(category) # ⑥ 创建新菜单并保存

menu = category_menus[category] # ⑦ 获取对应的菜单对象

action = menu.addAction(task["name"]) # ⑧ 在菜单下添加一个动作(菜单项)

action.setStatusTip(task.get("description", "")) # ⑨ 设置状态栏提示(鼠标悬停时显示)

# 使用 lambda 捕获当前 task,避免循环变量覆盖问题

action.triggered.connect(lambda checked, t=task: self.open_task_dialog(t)) # ⑩ 连接点击信号

# ===== 作用总结:根据 TASK_LIST 自动生成分类菜单,每个任务对应一个菜单项 =====

# ===== 第 3 部分:设置中央空白区域 =====

central_widget = QWidget() # ① 创建一个空白 QWidget 作为中央部件

self.setCentralWidget(central_widget) # ② 将其设置为主窗口的中央区域

layout = QVBoxLayout(central_widget) # ③ 为中央部件设置垂直布局

label = QLabel("请从上方菜单中选择要执行的数据处理任务") # ④ 创建一个提示标签

label.setAlignment(Qt.AlignCenter) # ⑤ 文字水平垂直居中

layout.addWidget(label) # ⑥ 将标签放入布局

# ===== 作用总结:主窗口中央显示一行提示文字,引导用户点击菜单 =====

# --------------------------------------------------------------------------

# 打开任务对话框(新增代码,三段式注解)

# --------------------------------------------------------------------------

def open_task_dialog(self, task_config):

"""

菜单项点击后执行的槽函数。

"""

# ===== 第 1 部分:创建对话框实例 =====

dialog = TaskDialog(task_config, self) # ① 实例化 TaskDialog,传入任务配置和主窗口作为父对象

# ===== 作用总结:生成对应任务的参数输入对话框 =====

# ===== 第 2 部分:以模态方式显示对话框 =====

dialog.exec() # ① exec() 方法使对话框成为模态窗口(必须关闭才能操作主窗口)

# ===== 作用总结:显示对话框,并阻塞主窗口交互,直到用户关闭对话框 =====

# ==============================================================================

# 【区域七:程序入口(新增代码,三段式注解)】

# ==============================================================================

if __name__ == "__main__":

# ===== 第 1 部分:创建 QApplication 实例 =====

app = QApplication(sys.argv) # ① 任何 PySide6 程序都必须有 QApplication 对象,sys.argv 用于处理命令行参数

# ===== 作用总结:初始化 Qt 应用程序框架 =====

# ===== 第 2 部分:创建并显示主窗口 =====

window = MainWindow() # ① 实例化我们定义的主窗口类

window.show() # ② 显示窗口

# ===== 作用总结:让主窗口出现在屏幕上 =====

# ===== 第 3 部分:启动事件循环 =====

sys.exit(app.exec()) # ① app.exec() 进入 Qt 主事件循环,等待用户操作;程序结束时返回退出码

# ===== 作用总结:保持程序运行,直到用户关闭主窗口 =====